using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Moq;

namespace UnitTesting.Fixturing.Moq
{
	class FixtureDefaultValueProvider
	{
		readonly IValueProvider _valueProvider;
		static readonly Assembly _moqAssembly = typeof (Mock).Assembly;
		static readonly Type _mockDefaultValueProviderType = _moqAssembly.GetType("Moq.MockDefaultValueProvider");
		static readonly Type _emptyDefaultValueProviderType = _moqAssembly.GetType("Moq.EmptyDefaultValueProvider");

		readonly Mock _mockOwner;
		Mock _mockDefaultValueProvider;
		object _methodCallReturn;
		readonly Dictionary<Type, object> _defaultValues;
		readonly IFreezable _freezable;
		internal Dictionary<MethodInfo, Mock> InnerMocks { get; private set; }
		public object Object { get; private set; }

		internal FixtureDefaultValueProvider(Mock mockOwner, IValueProvider valueProvider, IFreezable freezable)
		{
			_mockOwner = Ensure.IsNotNull(mockOwner, "mockOwner");
			_valueProvider = Ensure.IsNotNull(valueProvider, "valueProvider");
			_freezable = Ensure.IsNotNull(freezable, "freezable");

			SetMockDefaultValueProvider(new object[] {mockOwner});
			SetObject();
			SetupProvideDefaultMethod();
			_defaultValues = GetDefaultValues();
			InnerMocks = GetInnerMocks();
		}

		void SetMockDefaultValueProvider(object[] arguments)
		{
			_mockDefaultValueProvider = (Mock) CreateConstructor().Invoke(new object[] {arguments});
			_mockDefaultValueProvider.CallBase = true;
		}

		static ConstructorInfo CreateConstructor()
		{
			var mockGenericType = typeof (Mock<>).MakeGenericType(_mockDefaultValueProviderType);
			return mockGenericType.GetConstructor(new[] {typeof (object[])});
		}

		void SetObject()
		{
			var property = _mockDefaultValueProvider.GetType().GetProperty("Object", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
			Object = property.GetValue(_mockDefaultValueProvider, null);
		}

		void SetupProvideDefaultMethod()
		{
			CallSetup();
			CallReturns();
		}

		void CallSetup()
		{
			var setupMethod = CreateSetupMethod();
			var expression = CreateSetupExpression();
			_methodCallReturn = setupMethod.Invoke(null, new object[] {_mockDefaultValueProvider, expression, null});
		}

		static MethodInfo CreateSetupMethod()
		{
			var methods = typeof (Mock).GetMethods(BindingFlags.Static | BindingFlags.NonPublic);
			return methods.Single(x => x.Name == "Setup" && x.GetGenericArguments().Length == 2)
				.MakeGenericMethod(_mockDefaultValueProviderType, typeof (object));
		}

		static Expression CreateSetupExpression()
		{
			var funcType = typeof (Func<,>).MakeGenericType(_mockDefaultValueProviderType, typeof (object));
			return Expression.Lambda(funcType, CreateBody(), CreateParameters(funcType));
		}

		static Expression CreateBody()
		{
			var setupMethod = _mockDefaultValueProviderType
				.GetMethods(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance)[0];
			var instance = Expression.Variable(_mockDefaultValueProviderType);
			// ReSharper disable PossiblyMistakenUseOfParamsMethod
			return Expression.Call(instance, setupMethod, CreateMethodInfoArgument());
			// ReSharper restore PossiblyMistakenUseOfParamsMethod
		}

		static Expression CreateMethodInfoArgument()
		{
			//It.IsAny<MemberInfo>()
			var isAnyMethodOfMethodInfo = typeof (It).GetMethod("IsAny", BindingFlags.Public | BindingFlags.Static)
				.MakeGenericMethod(typeof (MethodInfo));
			return Expression.Call(isAnyMethodOfMethodInfo);
		}

		static IEnumerable<ParameterExpression> CreateParameters(Type funcType)
		{
			int paramCount = 0;
			var parameters = funcType.GetMethod("Invoke").GetParameters()
				.Select(param => Expression.Parameter(param.ParameterType, String.Concat("arg", paramCount++)));
			return parameters;
		}

		void CallReturns()
		{
			var methods = _methodCallReturn.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance);
			var returnsMethod = methods.Single(x => x.Name == "Returns" && x.GetGenericArguments().Length == 1)
				.MakeGenericMethod(typeof (MethodInfo));
			var delegateFunc = (Func<MethodInfo, object>) (ProvideDefault);
			returnsMethod.Invoke(_methodCallReturn, new object[] {delegateFunc});
		}

		internal virtual object ProvideDefault(MethodInfo member)
		{
			Ensure.IsNotNull(member, "member");

			var valueType = member.ReturnType;
			if (_defaultValues.ContainsKey(valueType))
			{
				return _defaultValues[valueType];
			}
			Mock mock;
			if (InnerMocks.TryGetValue(member, out mock))
			{
				return mock.Object;
			}
			var result = CreateTargetValue(valueType);

			if (TryGetMock(result, valueType, out mock))
			{
				SaveToInnerMocks(member, mock);
				SetMockSettingsAsOwner(mock, valueType);
			}
			return result;
		}

		object CreateTargetValue(Type valueType)
		{
			return _valueProvider.Create(valueType);
		}

		static bool TryGetMock(object result, Type valueType, out Mock mock)
		{
			mock = null;
			if (valueType.IsSealed) return false;

			var propertyInfo = result.GetType().GetProperty("Mock", typeof (Mock<>).MakeGenericType(valueType));
			if (propertyInfo == null) return false;

			var value = propertyInfo.GetValue(result, null);
			mock = (Mock) value;
			return true;
		}

		void SaveToInnerMocks(MethodInfo key, Mock value)
		{
			InnerMocks.Add(key, value);
		}

		void SetMockSettingsAsOwner(Mock mock, Type valueType)
		{
			if (!_freezable.IsFrozen(valueType))
			{
				mock.DefaultValue = _mockOwner.DefaultValue;
				mock.CallBase = _mockOwner.CallBase;
			}
		}

		Dictionary<Type, object> GetDefaultValues()
		{
			return (Dictionary<Type, object>)
			       // ReSharper disable PossibleNullReferenceException
			       _emptyDefaultValueProviderType
			       	.GetField("defaultValues", BindingFlags.NonPublic | BindingFlags.Instance)
			       	// ReSharper restore PossibleNullReferenceException
			       	.GetValue(Object);
		}

		Dictionary<MethodInfo, Mock> GetInnerMocks()
		{
			return (Dictionary<MethodInfo, Mock>)
			       typeof (Mock)
			       	.GetProperty("InnerMocks", BindingFlags.NonPublic | BindingFlags.Instance)
			       	.GetValue(_mockOwner, null);
		}
	}
}